package com.scpii.api.common.auth.token;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.annotation.Resource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.stereotype.Component;

import com.scpii.api.common.auth.ClientAuthentication;
import com.scpii.api.common.auth.SerializationUtils;
import com.scpii.api.domain.auth.AccessToken;
import com.scpii.api.domain.auth.RefreshToken;
import com.scpii.api.persistence.auth.AccessTokenMapper;
import com.scpii.api.persistence.auth.RefreshTokenMapper;

@Component
public class DefaultTokenStore implements TokenStore {

	private static final Log LOG = LogFactory.getLog(DefaultTokenStore.class);

	@Resource
	private AccessTokenMapper accessTokenMapper;

	@Resource
	private RefreshTokenMapper refreshTokenMapper;

	private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();

	@Override
	public ClientAuthentication readAuthentication(AuthAccessToken token) {
		return readAuthentication(token.getValue());
	}

	@Override
	public ClientAuthentication readAuthentication(String token) {
		ClientAuthentication authentication = null;

		try {
			AccessToken accessToken = accessTokenMapper
					.selectByPrimaryKey(extractTokenKey(token));
			if (accessToken == null) {
				throw new EmptyResultDataAccessException(1);
			}
			authentication = deserializeAuthentication(accessToken
					.getAuthentication());
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for token " + token);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize authentication for " + token);
			removeAccessToken(token);
		}

		return authentication;
	}

	@Override
	public void storeAccessToken(AuthAccessToken token,
			ClientAuthentication authentication) {
		String refreshToken = null;
		if (token.getRefreshToken() != null) {
			refreshToken = token.getRefreshToken().getValue();
		}
		AccessToken record = new AccessToken();
		record.setTokenId(extractTokenKey(token.getValue()));
		record.setToken(serializeAccessToken(token));
		record.setAuthenticationId(authenticationKeyGenerator
				.extractKey(authentication));
		record.setDeviceId(authentication.getAuthorizationRequest()
				.getDeviceId());
		record.setAppId(authentication.getAuthorizationRequest().getAppId());
		record.setAuthentication(serializeAuthentication(authentication));
		record.setRefreshToken(refreshToken);
		record.setCreateDt(new Date());
		accessTokenMapper.insert(record);
	}

	@Override
	public AuthAccessToken readAccessToken(String tokenValue) {
		AuthAccessToken oaAuth2AccessToken = null;
		try {
			AccessToken accessToken = accessTokenMapper
					.selectByPrimaryKey(extractTokenKey(tokenValue));
			if (accessToken == null) {
				throw new EmptyResultDataAccessException(1);
			}
			oaAuth2AccessToken = deserializeAccessToken(accessToken.getToken());
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for token " + tokenValue);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize access token for " + tokenValue);
			removeAccessToken(tokenValue);
		}
		return oaAuth2AccessToken;
	}

	@Override
	public void removeAccessToken(AuthAccessToken token) {
		removeAccessToken(token.getValue());
	}

	public void removeAccessToken(String tokenValue) {
		accessTokenMapper.deleteByPrimaryKey(extractTokenKey(tokenValue));
	}

	@Override
	public void storeRefreshToken(AuthRefreshToken refreshToken,
			ClientAuthentication authentication) {
		RefreshToken record = new RefreshToken();
		record.setTokenId(extractTokenKey(refreshToken.getValue()));
		record.setToken(serializeRefreshToken(refreshToken));
		record.setAuthentication(serializeAuthentication(authentication));
		record.setCreateDt(new Date());
		refreshTokenMapper.insert(record);
	}

	@Override
	public AuthRefreshToken readRefreshToken(String token) {
		AuthRefreshToken AuthRefreshToken = null;

		try {
			RefreshToken refreshToken = refreshTokenMapper
					.selectByPrimaryKey(extractTokenKey(token));
			if (refreshToken == null) {
				throw new EmptyResultDataAccessException(1);
			}
			AuthRefreshToken = deserializeRefreshToken(refreshToken.getToken());
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find refresh token for token " + token);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize refresh token for token " + token);
			removeRefreshToken(token);
		}

		return AuthRefreshToken;
	}

	@Override
	public ClientAuthentication readAuthenticationForRefreshToken(
			AuthRefreshToken token) {
		return readAuthenticationForRefreshToken(token.getValue());
	}

	public ClientAuthentication readAuthenticationForRefreshToken(String value) {
		ClientAuthentication authentication = null;

		try {
			RefreshToken refreshToken = refreshTokenMapper
					.selectByPrimaryKey(extractTokenKey(value));
			if (refreshToken == null) {
				throw new EmptyResultDataAccessException(1);
			}

			authentication = deserializeAuthentication(refreshToken
					.getAuthentication());
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for token " + value);
			}
		} catch (IllegalArgumentException e) {
			LOG.warn("Failed to deserialize access token for " + value);
			removeRefreshToken(value);
		}

		return authentication;
	}

	@Override
	public void removeRefreshToken(AuthRefreshToken token) {
		removeRefreshToken(token.getValue());
	}

	public void removeRefreshToken(String token) {
		refreshTokenMapper.deleteByPrimaryKey(extractTokenKey(token));
	}

	@Override
	public void removeAccessTokenUsingRefreshToken(AuthRefreshToken refreshToken) {
		removeAccessTokenUsingRefreshToken(refreshToken.getValue());
	}

	public void removeAccessTokenUsingRefreshToken(String refreshToken) {
		accessTokenMapper.deleteByRefreshToken(extractTokenKey(refreshToken));
	}

	@Override
	public AuthAccessToken getAccessToken(ClientAuthentication authentication) {
		AuthAccessToken AuthAccessToken = null;

		try {
			AccessToken accessToken = accessTokenMapper
					.selectByPrimaryKey(authenticationKeyGenerator
							.extractKey(authentication));
			if (accessToken == null) {
				throw new EmptyResultDataAccessException(1);
			}
			AuthAccessToken = deserializeAccessToken(accessToken
					.getAuthentication());
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.debug("Failed to find access token for authentication "
						+ authentication);
			}
		} catch (IllegalArgumentException e) {
			LOG.error("Could not extract access token for authentication "
					+ authentication);
		}

		if (AuthAccessToken != null
				&& !authentication.equals(readAuthentication(AuthAccessToken
						.getValue()))) {
			removeAccessToken(AuthAccessToken.getValue());
			storeAccessToken(AuthAccessToken, authentication);
		}
		return AuthAccessToken;
	}

	@Override
	public Collection<AuthAccessToken> findTokensByDeviceId(Long deviceId) {
		List<AuthAccessToken> AuthAccessTokens = null;
		try {
			List<AccessToken> accessTokens = accessTokenMapper
					.selectByDeviceId(deviceId);
			if (accessTokens == null || accessTokens.size() == 0) {
				throw new EmptyResultDataAccessException(1);
			}
			AuthAccessTokens = safeAccessToken(accessTokens);
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for deviceId " + deviceId);
			}
		}
		AuthAccessTokens = removeNulls(AuthAccessTokens);
		return AuthAccessTokens;
	}

	@Override
	public Collection<AuthAccessToken> findTokensByClientId(String clientId) {
		List<AuthAccessToken> AuthAccessTokens = null;

		try {
			List<AccessToken> accessTokens = accessTokenMapper
					.selectByAppId(clientId);
			if (accessTokens == null || accessTokens.size() == 0) {
				throw new EmptyResultDataAccessException(1);
			}
			AuthAccessTokens = safeAccessToken(accessTokens);
		} catch (EmptyResultDataAccessException e) {
			if (LOG.isInfoEnabled()) {
				LOG.info("Failed to find access token for clientId " + clientId);
			}
		}
		AuthAccessTokens = removeNulls(AuthAccessTokens);
		return AuthAccessTokens;
	}

	protected String extractTokenKey(String value) {
		if (value == null) {
			return null;
		}
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException(
					"MD5 algorithm not available.  Fatal (should be in the JDK).");
		}

		try {
			byte[] bytes = digest.digest(value.getBytes("UTF-8"));
			return String.format("%032x", new BigInteger(1, bytes));
		} catch (UnsupportedEncodingException e) {
			throw new IllegalStateException(
					"UTF-8 encoding not available.  Fatal (should be in the JDK).");
		}
	}

	public void setAuthenticationKeyGenerator(
			AuthenticationKeyGenerator authenticationKeyGenerator) {
		this.authenticationKeyGenerator = authenticationKeyGenerator;
	}

	private List<AuthAccessToken> removeNulls(List<AuthAccessToken> accessTokens) {
		List<AuthAccessToken> tokens = new ArrayList<AuthAccessToken>();
		for (AuthAccessToken token : accessTokens) {
			if (token != null) {
				tokens.add(token);
			}
		}
		return tokens;
	}

	private List<AuthAccessToken> safeAccessToken(List<AccessToken> accessTokens) {
		List<AuthAccessToken> AuthAccessTokens = new ArrayList<AuthAccessToken>();
		for (AccessToken accessToken : accessTokens) {
			try {
				AuthAccessTokens.add(deserializeAccessToken(accessToken
						.getAuthentication()));
			} catch (IllegalArgumentException e) {
				accessTokenMapper.deleteByPrimaryKey(accessToken.getTokenId());
				AuthAccessTokens.add(null);
			}
		}
		return AuthAccessTokens;
	}

	protected byte[] serializeAccessToken(AuthAccessToken token) {
		return SerializationUtils.serialize(token);
	}

	protected byte[] serializeRefreshToken(AuthRefreshToken token) {
		return SerializationUtils.serialize(token);
	}

	protected byte[] serializeAuthentication(ClientAuthentication authentication) {
		return SerializationUtils.serialize(authentication);
	}

	protected AuthAccessToken deserializeAccessToken(byte[] token) {
		return SerializationUtils.deserialize(token);
	}

	protected AuthRefreshToken deserializeRefreshToken(byte[] token) {
		return SerializationUtils.deserialize(token);
	}

	protected ClientAuthentication deserializeAuthentication(
			byte[] authentication) {
		return SerializationUtils.deserialize(authentication);
	}
}
